<?php

/*
 * Модель данных.
 * Абстрактный класс, требует реализации в классах-наследниках. 
 */

namespace T4;

use T4\MODEL\AdapterCollection;
use T4\MODEL\ElementFactory;

abstract class Model extends Standard {
	
	/**
	 * Имя поля первичного ключа по умолчанию
	 * Можно переопределять в конкретных моделях, но я бы не советовал это делать без нужды.
	 */
	const PRIMARY_KEY = 'id';

	/**
	 * Адаптер по умолчанию
	 */
	const DEFAULT_ADAPTER = 'mysql';

	/**
	 * Схема данных модели
	 * @var array Схема данных 
	 */
	protected static $schema = array();

	/**
	 * ----------------------------------------
	 * Установка и удаление моделей
	 * ----------------------------------------
	 */
	
	static public function install() {
		$adapter = AdapterCollection::getInstance(get_called_class());
		$adapter->install();
	}

	static public function uninstall() {
		$adapter = AdapterCollection::getInstance(get_called_class());
		$adapter->uninstall();
	}

	/**
	 * ----------------------------------------
	 * Статические методы, методы всей таблицы
	 * ----------------------------------------
	 */

	/**
	 * Выполняет произвольный запрос к БД
	 * Возвращает коллекцию объектов-записей из результата запроса
	 * @param string $query Запрос
	 * @return \T4\MODEL\Collection Коллекция объектов-моделей записей
	 */
	public static function query($query) {
		$adapter = AdapterCollection::getInstance(get_called_class());
		return $adapter->query($query);
	}

	/**
	 * Возвращает все записи из таблицы
	 * @param string $order Выражение, по которому будут отсортированы записи
	 * @return \T4\MODEL\Collection Коллекция объектов-моделей записей
	 */
	public static function findAll($order = null) {
		$adapter = AdapterCollection::getInstance(get_called_class());
		return $adapter->findAll($order);
	}

	/**
	 * Выборка из таблицы всех записей по значению поля
	 * @param string $field Имя поля
	 * @param mixed $value Значение поля
	 * @param string $where Условие для выборки
	 * @param string $order Выражение, по которому будут отсортированы записи
	 * @param string $limit Выражение ограничения числа записей в запросе
	 * @return T4\MODEL\Collection Коллекция объектов-моделей записей 
	 */
	public static function findByField($field, $value, $where = null, $order = null, $limit = null) {
		$adapter = AdapterCollection::getInstance(get_called_class());
		return $adapter->findByField($field, $value, $where, $order, $limit);
	}

	/**
	 * Выборка одной записи по условиям поиска и сортировки
	 * @param type $where Условие поиска
	 * @param type $order Порядок сортировки
	 * @return T4\Model Модель записи
	 */
	public static function findOne($where = null, $order = null) {
		$adapter = AdapterCollection::getInstance(get_called_class());
		return $adapter->findOne($where, $order);
	}

	/**
	 * Выборка из таблицы одной записи по значению поля
	 * @param mixed $value Значение поля
	 * @return T4\Model Модель записи 
	 */
	public static function findOneByField($field, $value, $where = null, $order = null) {
		$adapter = AdapterCollection::getInstance(get_called_class());
		return $adapter->findOneByField($field, $value, $where, $order);
	}

	/**
	 * Выборка из таблицы одной записи по первичному ключу
	 * @param int $value Значение первичного ключа
	 * @return T4\Model Модель записи 
	 */
	public static function findByPK($value) {
		return static::findOneByField(static::PRIMARY_KEY, $value);
	}

	/**
	 * 
	 * Реализовано через метод self::__callStatic()
	 * 
	 * public static function findOneBy*($value)
	 * public static function findBy*($value)
	 * 
	 */
	public static function __callStatic($name, $arguments) {

		switch (true) {

			/**
			 * findOneBy{Field}
			 */
			case preg_match("/^findOneBy([a-z0-9]+)/i", $name, $matches):
				$field = strtolower($matches[1]);
				return self::findOneByField($field, $arguments[0], isset($arguments[1]) ? $arguments[1] : null, isset($arguments[2]) ? $arguments[2] : null);
				break;

			/**
			 * findBy{Field}
			 */
			case preg_match("/^findBy([a-z0-9]+)/i", $name, $matches):
				$field = strtolower($matches[1]);
				return self::findByField($field, $arguments[0], isset($arguments[1]) ? $arguments[1] : null, isset($arguments[2]) ? $arguments[2] : null, isset($arguments[3]) ? $arguments[3] : null);
				break;

			default:
				throw new Exception('Вызов неопределенного статического метода модели!');
				break;
			
		}
		
	}

	/**
	 * ----------------------------------------
	 * Динамические методы, методы объекта-записи
	 * ----------------------------------------
	 */

	/**
	 * 
	 * Реализовано через метод self::__call()
	 * 
	 * public function set*($value)
	 * 
	 */
	public function __call($name, $arguments) {

		switch (true) {

			/**
			 * set{Field}
			 */
			case preg_match("/^set([a-z0-9]+)/i", $name, $matches):
				$field = strtolower($matches[1]);
				$this->{$field} = $arguments[0];
				return $this;
				break;
			
		}
		
	}

	public function delete() {
		$adapter = AdapterCollection::getInstance(get_called_class());
		$adapter->setModel($this);
		$adapter->delete();
	}

	public function save() {

		$adapter = AdapterCollection::getInstance(get_called_class());
		$adapter->setModel($this);

		if ($this->__isset(static::PRIMARY_KEY)) {
			$adapter->update();
		} else {
			$adapter->insert();
		}

		return $this;
		
	}

	/**
	 * ----------------------------------------
	 * Служебные методы
	 * ----------------------------------------
	 */

	/**
	 * Приводит имя к множественному числу (англ.)
	 * @param string $name Некое имя
	 * @return string Множественное число этого имени
	 */
	protected final function pluralize($name) {
		return $name . 's';
	}

	/**
	 * ----------------------------------------
	 * Методы для работы со схемой данных
	 * ----------------------------------------
	 */

	/**
	 * Получение схемы данных
	 * При отсутствии данных о таблице происходит попытка автоопределения таблицы
	 * @return array Схема данных
	 */
	public final static function getSchema() {
		$schema = &static::$schema;
		if (!isset($schema['table']) || empty($schema['table']))
			$schema['table'] = static::guessTable();
		return $schema;
	}

	/**
	 * Возвращает имя таблицы, связанной с данным классом
	 * @return string Имя таблицы 
	 */
	public final static function getTable() {
		$schema = &static::getSchema();
		return $schema['table'];
	}

	/**
	 * Проверка наличия в схеме информации о полях данных
	 * @return boolean 
	 */
	public final static function hasSchemaColumns() {
		$schema = static::getSchema();
		return ( isset($schema['columns']) && !empty($schema['columns']) );
	}

	/**
	 * Получение всех имен полей данных в схеме
	 * @return array 
	 */
	public final static function getSchemaColumns() {
		$schema = static::getSchema();
		$ret = array();
		if (static::hasSchemaColumns()) {
			$ret = array_keys($schema['columns']);
			$id = static::PRIMARY_KEY;
			if (!in_array($id, $ret))
				array_unshift($ret, $id);
		}
		return $ret;
	}

	/**
	 * Проверка наличия в схеме информации о связях с другими моделями
	 * @return boolean 
	 */
	public final static function hasSchemaRelations() {
		$schema = static::getSchema();
		return ( isset($schema['relations']) && !empty($schema['relations']) );
	}

	/**
	 * Вычисляет имя таблицы по полному имени класса
	 * Используется для автоопределения имени таблицы
	 * @return string Вычисленное имя таблицы
	 */
	protected final static function guessTable() {

		$className = get_called_class();
		$classNameParts = explode('\\', $className);

		switch (true) {
			case ( count($classNameParts) == 4 && $classNameParts[0] == 'APPS' ):
				$table = strtolower($classNameParts[1]) . '_' . self::pluralize(strtolower($classNameParts[3]));
				break;

			default:
				throw new Exception('Неверное имя класса-модели!');
				break;
		}

		return $table;
		
	}

	/**
	 * Магические методы для прямого доступа к данным
	 */
	
	public function __isset($name) {
		$schema = static::getSchema();
		return
				isset($this->data[$name])
			||	isset($schema['columns'][$name])
			||	isset($schema['relations'][$name]);
	}
	
	public function __set($name, $value) {

		$schema = static::getSchema();

		/*
		 * Для первичного ключа отдельная логика
		 * Его можно только задать, один раз, изменить - нельзя
		 */
		if ($name == static::PRIMARY_KEY) {
			if ( isset($this->data[$name])  ) {
				throw new Exception('Попытка изменить значение первичного ключа!');
			} else {
				$element = ElementFactory::getInstance($name, array('type' => 'int'));
				$element->setData($value);
				$this->data[$name] = $element;
				return;
			}			
		}

		/**
		 * Проверка на наличие в схеме
		 */
		if ( static::hasSchemaColumns() || static::hasSchemaRelations() ) {
			if ( !in_array($name, array_keys($schema['columns'] ?: array())) && !in_array($name, array_keys($schema['relations'] ?: array())) ) {
				return;
			}
		}

		/**
		 * Отношение или простое поле?
		 */
		if ( static::hasSchemaRelations() && in_array($name, array_keys($schema['relations'] ?: array())) ) {
			
			$relationClass = $schema['relations'][$name]['model'];
			if ( is_object($value) && $value instanceof $relationClass ) {
				$this->data['name'] = $value;
			} else {			
				$id = intval($value);			
				$this->data[$name] = $relationClass::findByPK($id);
			}
			
		} else {
			if (!isset($this->data[$name])) {
				$this->data[$name] = ElementFactory::getInstance($name, $schema['columns'][$name]);
			}
			$this->data[$name]->setData($value);
		}

	}

	public function __get($name) {
		$schema = static::getSchema();
		if ( !isset($this->data[$name]) && isset($schema['columns'][$name]) ) {
			$this->data[$name] = ElementFactory::getInstance($name, $schema['columns'][$name]);
		}
		return $this->data[$name];
	}
	
	/*
	 * ----------------------------------------
	 * Связь с хелперами
	 * ----------------------------------------
	 */
	
	public function form($mode='edit', $action='', $method='post') {
		return HTML\Helper::form($this, $mode, $action, $method);
	}

}